-
-
Notifications
You must be signed in to change notification settings - Fork 305
Move object dunders from FunctionModel
to ObjectModel
#2847
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
Signed-off-by: Emmanuel Ferdman <[email protected]>
Codecov Reportβ Patch coverage is
β Your patch check has failed because the patch coverage (93.02%) is below the target coverage (100.00%). You can increase the patch coverage or adjust the target coverage. Additional details and impacted files@@ Coverage Diff @@
## main #2847 +/- ##
==========================================
- Coverage 93.37% 93.33% -0.04%
==========================================
Files 92 92
Lines 11150 11168 +18
==========================================
+ Hits 10411 10424 +13
- Misses 739 744 +5
Flags with carried forward coverage won't be shown. Click here to find out more.
π New features to boost your workflow:
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As can be seen from the docs you link, most of these should live on ObjectModel
instead as they come from object
.
We tried to do so before in #1519 but failed. Perhaps you want to have another look at that PR and see if you can fix the issue we faced there?
@DanielNoord Thanks! Iβll take a look at this. From that thread, it seems you were close to a solution, except for one test case that didnβt pass. Do you remember which test it was? |
@emmanuel-ferdman I believe it was an issue in the |
Signed-off-by: Emmanuel Ferdman <[email protected]>
for more information, see https://pre-commit.ci
Signed-off-by: Emmanuel Ferdman <[email protected]>
@DanielNoord I've put together an initial solution for moving object dunders from The solution uses a placeholder pattern:
How it works:
Thanks for any feedback π |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is amazing, very well written PR and a nice set of tests.
Well done on getting this to pass all tests. I have left some comments, but would really like to help you push this over the line :)
# Builtin dunder methods have empty bodies, return Uninferable. | ||
if ( | ||
self.root().qname() == "builtins" | ||
and self.name.startswith("__") | ||
and self.name.endswith("__") | ||
and self.parent | ||
and self.parent.__class__.__name__ == "ClassDef" | ||
): | ||
yield util.Uninferable | ||
return |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think I sort of understand this change, but don't understand why we need to special case it like this.
Can we replace the checks with a look up in special_attributes
? Or doesn't that work?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We need this case because infer_call_result()
has to decide what a call returns, and for built-in dunder methods there is no Python body to inspect (since they're implemented in C). If we tried to parse return
nodes we would either fail or produce incorrect results.
special_attributes
only tells us that an attribute exists (a placeholder), not whether there is a Python body to infer a return value from. Therefore it cannot replace the empty-body/builtins check used here.
But on second thought, I simplified the case by explicitly checking len(self.body) == 0
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wouldn't it be okay if we always did this for len(self.body) == 0
? Is there a function where len(self.body) == 0
that is inferable? Or is as ...
or pass
as body also len(self.body) == 0
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think we can't simplify to just len(self.body) == 0
because both builtin dunders (__eq__
, __hash__
) and non-dunder builtins (getattr
, len
, print
) have empty bodies. If we return Uninferable
for all empty-body functions, it breaks tests - specifically getattr(int, 'missing_attr')
returns Uninferable
instead of raising InferenceError
(test test_attribute_missing
fails). Regarding pass/...
- they create len(body) = 1
, not 0. Only C builtins have truly empty bodies.
What I think I can simplify is to drop the self.root().qname() == "builtins"
check since len(body) == 0
already guarantees it's a builtin
. All tests pass (both in astroid and pylint) with this simplification. However, I'm not 100% certain there isn't some corner case we're missing with that removal. Let me know if you prefer me to drop this check π
special_attr = self.special_attributes.lookup(name) | ||
if not isinstance( | ||
special_attr, (util.UninferableBase, node_classes.Unknown) | ||
): | ||
result = [special_attr] | ||
return result |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is the effect of this? What will we eventually return if the if
is not True
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This block is a short-circuit used when there are no concrete locals/ancestor definitions: originally it always returned special_attributes.lookup(name)
(even if that was Unknown
or Uninferable
). The new behavior only returns the special_attr
when it is a concrete value (not node_classes.Unknown
or util.UninferableBase
), preventing a placeholder from being returned prematurely and masking an override in a metaclass/base class. If the if
is not true we continue the normal lookup (metaclass lookup, collect/filter locals/ancestors) and ultimately return any real definitions found - otherwise an AttributeInferenceError
is raised. Placeholders (Unknown
/Uninferable
) therefore mean βkeep looking,β not βreturn this as the final result.β
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unkown
feels like a nice placeholder for "keep looking", Uninferable
generally means "stop inferring, you won't be able to". Can we make this only check for Unknown
? Or does that not work?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, good idea! ObjectModel
only returns Unknown
as placeholders, never Uninferable
. I changed both checks (in ClassDef.getattr()
and BaseInstance.getattr()
) to only check for Unknown
. If Uninferable
somehow appeared from special_attributes
, we should treat it as a final value and return it, not skip it. All tests pass with this change.
except AttributeInferenceError as exc: | ||
if self.special_attributes and name in self.special_attributes: | ||
return [self.special_attributes.lookup(name)] | ||
special_attr = self.special_attributes.lookup(name) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Similar question as above
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same pattern as the ClassDef.getattr()
check - when special_attr
is Unknown
or Uninferable
, we skip the early return and continue to the if lookupclass: block which searches the class for the attribute. This ensures we find actual implementations in classes rather than stopping at the Unknown
placeholder.
# Special case: __hash__ = None overrides ObjectModel for unhashable types. | ||
if name == "__hash__" and value is None: | ||
_attach_local_node(node, nodes.const_factory(value), name) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this be the case for all attributes where value is None
? Or only hash
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe only for __hash__
. In Python __hash__ = None
has a special semantic meaning - it explicitly marks a type as unhashable and must override the inherited object.__hash__
. Other attributes set to None
do not carry this override semantics and should not implicitly replace special_attributes
entries, because that could hide real implementations or metadata. So I belive we should have this special-case restricted to __hash__
only.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps we can link that documentation here? It would prevent me from wondering why this special case is here the next time.
And thanks for the link, TIL about something I knew implicitly but now know where to find the documentation for that feature π Appreciated!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good idea - added the documentation link to both places π
child = object_build_datadescriptor(node, member) | ||
elif isinstance(member, tuple(node_classes.CONST_CLS)): | ||
if alias in node.special_attributes: | ||
# Special case: __hash__ = None overrides ObjectModel for unhashable types. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same question as above
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same reasoning as the previous hash question - hash = None
is the only attribute where None
has special semantic meaning in Python (marks unhashable types). Other None
values don't need this override behavior.
# Builtin dunder methods now return Uninferable instead of raising InferenceError | ||
result = next(lenmeth.infer_call_result(None, None)) | ||
assert result is util.Uninferable |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, I think the changes above now make more sense to me.
pylint
works correctly with this change? Thanks for testing that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes - pylint
works correctly (verified by running pylint
test suite with astroid changes). pylint
handles Uninferable
in two ways: (1) explicit isinstance(value, util.UninferableBase)
checks (e.g., pylint/checkers/typecheck.py:1365-1369
, and (2) Uninferable
is falsy like None
, so if inferred: checks skip both. The behavior is identical - when builtin dunders can't be inferred, pylint skips type checking whether it gets None
from an exception or Uninferable
from the return value.
) | ||
inferred = next(eq_result.infer()) | ||
assert isinstance(inferred, nodes.Const) | ||
assert inferred.value == "custom equality" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice!
Signed-off-by: Emmanuel Ferdman <[email protected]>
FunctionModel
to ObjectModel
Signed-off-by: Emmanuel Ferdman <[email protected]>
Type of Changes
Description
This PR does:
This PR moves object dunder methods from
FunctionModel
toObjectModel
asUnknown
placeholders, making them available to all object types.Fixes #2742 #2741.